package org.damazio.bhtranscams;

import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnCancelListener;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.location.LocationProvider;
import android.os.Bundle;
import android.widget.Toast;

import com.google.android.maps.GeoPoint;

/**
 * Logic for picking the camera closest to a location
 * (either the user's current location or a manually specified one).
 *
 * @author rdamazio
 */
public class ClosestCameraPicker {
  /**
   * The minimum accuracy of the location result to let it be used.
   */
  private static final float MAX_ACCURACY_METERS = 256;

  private final double maxDistance;
  private final Context context;

  private LocationManager locationManager;
  private ProgressDialog locationDialog;

  /**
   * Callback for the location manager.
   */
  private class ClosestLocationListener implements LocationListener {
    public void onStatusChanged(String provider, int status, Bundle extras) {
      if (status == LocationProvider.OUT_OF_SERVICE) {
        // Service won't be available - show an error and give up
        Toast.makeText(context, R.string.locationFailed, Toast.LENGTH_SHORT).show();

        stopLocationSearch(this);
      }
    }

    public void onProviderEnabled(String provider) {
      // Do nothing
    }

    public void onProviderDisabled(String provider) {
      // Do nothing
    }
    
    public void onLocationChanged(Location location) {
      if (location.hasAccuracy() && location.getAccuracy() > MAX_ACCURACY_METERS) {
        // We have a location but it's not very precise yet
        return;
      }

      stopLocationSearch(this);

      GeoPoint point = new GeoPoint((int) (location.getLatitude() * 1E6),
                                    (int) (location.getLongitude() * 1E6));
      showClosestCamera(point);
    }
  };

  /**
   * Default constructor.
   *
   * @param maxDistance the maximum distance from a camera in which that camera
   *        can still be considered for closest-camera selection
   */
  public ClosestCameraPicker(Context context, double maxDistance) {
    this.context = context;
    this.maxDistance = maxDistance;
  }

  /**
   * Shows the camera closest to the user's present location.
   * This runs asynchronously, as it needs to first obtain the user's location.
   */
  public void showClosestCameraToLocation() {
    startLocationSearch();
  }

  /**
   * Starts trying to obtain the user's location.
   * Once the location is found, the closest camera will be displayed.
   */
  private void startLocationSearch() {
    if (locationDialog != null) {
      // Already trying to get locsest location
      return;
    }

    final ClosestLocationListener locationListener = new ClosestLocationListener();
    OnCancelListener cancelListener = new OnCancelListener() {
      public void onCancel(DialogInterface dialog) {
        stopLocationSearch(locationListener);
      }
    };

    locationDialog = ProgressDialog.show(context, "", context.getString(R.string.loadingLocation), true, true, cancelListener);
    locationManager = (LocationManager)context.getSystemService(Context.LOCATION_SERVICE);
    locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, Long.MAX_VALUE, Float.MAX_VALUE, locationListener);
  }

  /**
   * Stops reporting locations. whether one has already been found or not.
   *
   * @param listener the location listener to stop receiving locations with
   */
  private void stopLocationSearch(LocationListener listener) {
    // TODO: If the user switches to another app (e.g. presses home), we need to call this too
    if (locationManager == null || locationDialog == null) return;

    locationManager.removeUpdates(listener);
    locationDialog.dismiss();
    locationManager = null;
    locationDialog = null;
  }

  /**
   * Displays the camera closest to the given point.
   * If there's no camera close enough, this becomes a no-op.
   */
  public void showClosestCamera(GeoPoint p) {
    double closestCameraDistance = Double.MAX_VALUE;
    Cameras closestCamera = null;
    for (Cameras camera : Cameras.values()) {
      double cameraDistance = distanceToCamera(p, camera);
      if (cameraDistance < closestCameraDistance && cameraDistance <= maxDistance) {
        closestCameraDistance = cameraDistance;
        closestCamera  = camera;
      }
    }

    if (closestCamera != null) {
      BHTransCamView.startCameraViewActivity(context, closestCamera);
    } else {
      Toast.makeText(context, R.string.locationsTooFar, Toast.LENGTH_SHORT).show();
    }
  }

  /**
   * Calculates and returns the distance (in degrees) from the given point to
   * the given camera's location.
   */
  private double distanceToCamera(GeoPoint point, Cameras camera) {
    double latDiff = ((double) point.getLatitudeE6()) / 1E6 - camera.getLatitude();
    double longDiff = ((double) point.getLongitudeE6()) / 1E6 - camera.getLongitude();
    return Math.sqrt(latDiff * latDiff + longDiff * longDiff);
  }
}
